﻿#if UNITY_2019_4_OR_NEWER

using UnityEngine;
using System.Threading;
using System.Reflection;
using System.Collections;

#if UNITY_EDITOR
using UnityEditor;
#endif

namespace Devil
{
    /// <summary>
    /// 初始化执行顺序
    /// </summary>
    public enum InitializeOrder
    {
        First = -10000,
        BeforeDefault = -1000,
        Default = 0,
        AfterDefault = 1000,
        Finalize = 10000,
    }

    /// <summary>
    /// 功能等效于 Unity 的 [InitializeOnLoadMethod] 属性，但增加了执行顺序和执行线程的配置，允许按顺序执行，或多线程执行
    /// （PS：Finalize 除外， 主线程的执行与非主线程的执行不具有执行顺序的比较，执行顺序仅限主线程间或者非主线程间）
    /// </summary>
    [System.AttributeUsage(System.AttributeTargets.Method, AllowMultiple = false)]
    public class InitializeOnLoadMethodParallelAttribute : System.Attribute
    {
        public InitializeOrder ExecutionOrder { get; set; }
        public bool IsOnMainThread { get; set; }
    }

    [DefaultExecutionOrder(-1000), AddComponentMenu("")]
    public class ParallelUnity : MonoBehaviour
    {
        static readonly object sync_lock = new object();
        public static bool IsMainThread => ParallelDispatcher.IsMainThread;

        static RuntimePlatform sPlatform;
        public static RuntimePlatform platform { get { return sPlatform; } }

        static bool sDebugBuild;
        public static bool isDebugBuild { get { return sDebugBuild; } }
        static string sDeviceID;
        public static string deviceIdentifier { get { return sDeviceID; } }

        static bool sPlaying = true;
        public static bool isPlaying { get { return sPlaying; } }

        static string sPersistentDataPath;
        public static string persistentDataPath { get { return sPersistentDataPath; } }

        static string sStreamingPath;
        public static string streamingAssetsPath { get { return sStreamingPath; } }

        static string sDataPath;
        public static string dataPath { get { return sDataPath; } }

        static string sTempPath;
        public static string tempPath { get { return sTempPath; } }

        static float sTimeScale;
        static float sTime;
        static float sUnscaledTime;
        static int sFrames;
        public static float timeScale { get { return sTimeScale; } }
        public static float unscaledTime { get { return sUnscaledTime; } }
        public static float time { get { return sTime; } }
        public static int frameCount { get { return sFrames; } }

        static System.Random sRandom;
        public static float GetRandomValue()
        {
            lock (sync_lock)
            {
                if (sRandom == null)
                    sRandom = new System.Random(ParallelUtils.GenUniqueID());
            }
            return (float)sRandom.NextDouble();
        }

        public static int GetRandomValue(int min, int max)
        {
            lock (sync_lock)
            {
                if (sRandom == null)
                    sRandom = new System.Random(ParallelUtils.GenUniqueID());
            }
            return sRandom.Next(min, max);
        }

        public static float GetRandomValue(float min, float max)
        {
            lock (sync_lock)
            {
                if (sRandom == null)
                    sRandom = new System.Random(ParallelUtils.GenUniqueID());
            }
            var v = sRandom.NextDouble();
            return (max - min) * (float)v + min;
        }

        /// <summary>
        /// 安装/初始化
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <param name="instName"></param>
        public static void Setup<T>(string instName) where T : ParallelUnity
        {
            if (sInstance == null)
            {
                if (string.IsNullOrEmpty(instName))
                    instName = string.Format("[{0}]", typeof(T).Name);
                var go = new GameObject(instName);
                DontDestroyOnLoad(go);
#if UNITY_EDITOR
                go.hideFlags |= HideFlags.DontSave | HideFlags.NotEditable;
#endif
                sInstance = go.AddComponent<T>();
                //Initialize();
#if UNITY_EDITOR
                Debug.LogFormat("{0} was setup and named as \"{1}\"", typeof(T).Name, instName);
#endif
            }
        }

        public static void DestroyAutomatic(Object go, bool deleteAsset = false)
        {
            if (ParallelDispatcher.IsMainThread)
            {
                if (go == null)
                    return;
                if (go is Component com)
                    go = com.gameObject;
#if UNITY_EDITOR
                if (!Application.isPlaying)
                {
                    if (deleteAsset && AssetDatabase.Contains(go))
                        AssetDatabase.RemoveObjectFromAsset(go);
                    Object.DestroyImmediate(go, deleteAsset);
                }
                else
#endif
                    Object.Destroy(go);
            }
            else
            {
                System.Action act = () =>
                {
                    if (go == null)
                        return;
#if UNITY_EDITOR
                    if (!Application.isPlaying)
                        Object.DestroyImmediate(go, deleteAsset);
                    else
#endif
                        Object.Destroy(go);

                };
                act.Schedule(true);
            }
        }

        [System.Diagnostics.Conditional("UNITY_EDITOR")]
        public static void AssertMainThread(string msg)
        {
            Debug.Assert(ParallelDispatcher.IsMainThread, msg);
        }

#if UNITY_EDITOR
        //static System.Action OnInstallized;

        class ParallelInitialize : IYieldCommand, ICommand
        {
            internal int order;
            internal bool mainThread;
            internal MethodInfo method;
            internal bool isImmediate
            {
                get
                {
                    if (!mainThread || order >= (int)InitializeOrder.Finalize)
                        return false;
                    var ret = method.ReturnType;
                    return ret == null || ret == typeof(void);
                }
            }

            internal ParallelDispatcher.Handler Schedule(ParallelDispatcher.Handler prev)
            {
                var ret = method.ReturnType;
                if (ret == null || ret == typeof(void))
                    return ((ICommand)this).Schedule(mainThread, prev);
                else
                    return ((IYieldCommand)this).Schedule(mainThread, prev);
            }

            IEnumerator IYieldCommand.Execute()
            {
                var obj = method.Invoke(null, null);
                yield return obj;
                //Debug.LogFormat("[{0}] Initialize: {1}.{2}()", ParallelDispatcher.IsMainThread ? "Main" : Thread.CurrentThread.ManagedThreadId.ToString("X8"),
                //    method.DeclaringType.Name, method.Name);
            }

            void ICommand.Execute()
            {
                method.Invoke(null, null);
            }
        }

        [MenuItem("Parallel Commands/Initialize On Load Method", validate = true)]
        static bool CanInstallForEditor()
        {
            return !EditorApplication.isPlayingOrWillChangePlaymode;
        }

        [InitializeOnLoadMethod, MenuItem("Parallel Commands/Initialize On Load Method")]
        static void InstallForEditor()
        {
            IsEditorInitialized = false;
            Initialize();
            EditorApplication.update -= EditorUpdate;
            EditorApplication.update += EditorUpdate;
            EditorApplication.playModeStateChanged -= EditorChangePlayMode;
            EditorApplication.playModeStateChanged += EditorChangePlayMode;

            var assemblies = System.AppDomain.CurrentDomain.GetAssemblies();
            MinHeap<ParallelInitialize> inits = new MinHeap<ParallelInitialize>(16, (a) => a.order);
            var flags = BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Static | BindingFlags.InvokeMethod;
            foreach (var assembly in assemblies)
            {
                var types = assembly.GetTypes();
                foreach (var tp in types)
                {
                    var methods = tp.GetMethods(flags);
                    foreach (var mtd in methods)
                    {
                        var cfg = mtd.GetCustomAttribute<InitializeOnLoadMethodParallelAttribute>();
                        if (cfg != null)
                        {
                            if (tp.ContainsGenericParameters)
                            {
                                Debug.LogError($"The InitializeOnLoadMethodParallelAttribute is not support for generic type declare ({tp.Name}.{mtd.Name})");
                                break;
                            }
                            else
                            {
                                ParallelInitialize init = new ParallelInitialize();
                                init.order = (int)cfg.ExecutionOrder;
                                init.mainThread = cfg.IsOnMainThread;
                                init.method = mtd;
                                inits.Add(init);
                            }
                        }
                    }
                }
            }
            int order = 0;
            ParallelDispatcher.Handler prev = default;
            ParallelDispatcher.CombinedHandler comb = ParallelUtils.NewCombination("Initialize On Load Method");
            while (inits.Count > 0)
            {
                var mtd = inits.RemoveTop();
                if (mtd.isImmediate)
                {
                    try
                    {
                        ((ICommand)mtd).Execute();
                    }
                    catch (System.Exception e)
                    {
                        Debug.LogException(e);
                    }
                }
                else
                {
                    if (order != mtd.order)
                    {
                        prev = comb.GetHandler();
                        comb = ParallelUtils.NewCombination("Initialize On Load Method");
                    }
                    var job = mtd.Schedule(prev);
                    comb.Combine(job);
                }
            }
            var handler = comb.GetHandler();
            ParallelUtils.Schedule(() => IsEditorInitialized = true, false, handler);
            if (handler.IsAlive)
                handler.SampleExecuteTime();
        }

        private static void EditorChangePlayMode(PlayModeStateChange change)
        {
            if (change == PlayModeStateChange.ExitingPlayMode)
            {
                var inst = sInstance;
                sInstance = null;
                if (inst != null)
                    DestroyAutomatic(inst);
            }
            //else if (change == PlayModeStateChange.EnteredEditMode)
            //{
            //    ParallelDispatcher.QuitApplication();
            //    ParallelDispatcher.InitApplicationData();
            //}
        }

        static void EditorUpdate()
        {
            ParallelDispatcher.SetPause(EditorApplication.isPaused && EditorApplication.isPlaying);
            if (sInstance == null)
            {
                ParallelDispatcher.GetInstance()?.MainUpdate(Time.deltaTime);
            }
        }


        [MenuItem("Parallel Commands/Print Parallel Cmds")]
#endif
        public static void PrintCmdInfo()
        {
            ThreadPool.QueueUserWorkItem((t) =>
            {
                var st = ParallelDispatcher.GetInstance();
                if (st != null)
                {
                    var str = st.GetCommandsInfo();
                    Debug.Log(str);
                }
            });
        }

        static ParallelUnity sInstance;
        public static ParallelUnity Instance => sInstance;
        public static bool IsEditorInitialized { get; private set; }

        static void Initialize()
        {
            sPlaying = Application.isPlaying;
            sPlatform = Application.platform;
            sDebugBuild = Debug.isDebugBuild;
            sDataPath = Application.dataPath;
            sStreamingPath = Application.streamingAssetsPath;
            sTimeScale = Time.timeScale;
            sTime = Time.time;
            sUnscaledTime = Time.unscaledTime;
            sFrames = Time.frameCount;
            sDeviceID = SystemInfo.deviceUniqueIdentifier;
            sTempPath = NativeFileSystem.Combine(Application.persistentDataPath, "temp");
#if UNITY_STANDALONE || UNITY_EDITOR
            sPersistentDataPath = NativeFileSystem.Combine(System.Environment.GetFolderPath(System.Environment.SpecialFolder.MyDocuments),
                Application.companyName, Application.productName);
#else
            sPersistentDataPath = Application.persistentDataPath;
#endif
            ParallelDispatcher.InitApplicationData();
            Debug.Log($"OS: {SystemInfo.operatingSystem} ({SystemInfo.operatingSystemFamily})\nCPU: {SystemInfo.processorType}\nRAM: {SystemInfo.systemMemorySize}");
        }

        protected virtual void Awake()
        {
            if (sInstance == null)
            {
                Initialize();
                sInstance = this;
            }
            DontDestroyOnLoad(gameObject);
        }

        protected virtual void OnEnable()
        {
            if (sInstance != null && sInstance != this)
            {
                enabled = false;
                throw new System.Exception("There are more than one ParallelUnity in the scene.");
            }
            //else
            //{
            //    Initialize();
            //    sInstance = this;
            //}
        }

        protected virtual void OnDisable()
        {
            if (sInstance == this)
            {
                sInstance = null;
                ParallelDispatcher.QuitApplication();
            }
        }


        protected virtual void Update()
        {
            sTimeScale = Time.timeScale;
            sTime = Time.time;
            sUnscaledTime = Time.unscaledTime;
            sFrames = Time.frameCount;
            ParallelDispatcher.GetInstance()?.MainUpdate(Time.deltaTime);
        }

    }

    /// <summary>
    /// 帧快照数据（缓存的帧数据）
    /// </summary>
    /// <typeparam name="T"></typeparam>
    public class FrameShot<T> : System.IDisposable
    {
        readonly object mLock;
        bool mMainThreadInvoke;
        int mFrameId;
        T mResult;
        ValueDelegate<T> mValueGetter;
        int mCacheFrames;

        public FrameShot(ValueDelegate<T> getter, int cacheFrames = 2, bool mainThreadOnly = false)
        {
            mMainThreadInvoke = mainThreadOnly;
            mValueGetter = getter;
            mCacheFrames = cacheFrames > 1 ? cacheFrames : 1;
            mFrameId = -mCacheFrames - 1;
            mLock = new object();
        }

        public T GetValue()
        {
            if (!mMainThreadInvoke || ParallelDispatcher.IsMainThread)
            {
                lock (mLock)
                {
                    var id = ParallelUnity.frameCount;
                    if (id < mFrameId || id >= mFrameId + mCacheFrames)
                    {
                        mFrameId = id;
                        mResult = mValueGetter();
                    }
                }
            }
            return mResult;
        }

        public void Dispose()
        {
            mFrameId = 0;
            mValueGetter = null;
            mResult = default;
        }

        ~FrameShot()
        {
            mFrameId = 0;
            mValueGetter = null;
            mResult = default;
        }

        public static implicit operator T(FrameShot<T> buf)
        {
            return buf == null ? default : buf.GetValue();
        }
    }
}

#endif